<?php
/**
*   Abstract class which facilitates PHP's `OOP' usage
*   Defines all PHP magic methods except clone,construct,destruct,invoke
*
*/

abstract class Yz_Class
    implements ArrayAccess
{

    /** Default to return Object<className>
    *   @return string
    */
    public function __toString()
    {
        $x = get_called_class();
        return "Object<$x>";
    } // __toString


    /** make a virtual subclass of this class with a given name in a given namespace
     *  @param string $newNamespace
     *  @param string $newName
     *  @return string new class name
     */
    public static function virtual($newNamespace, $newName)
    {
        $class = get_called_class();
        if (static::namespaceName() === $newNamespace) {
            return $class;
        }
        $eval = <<<c
namespace {$newNamespace};
class {$newName} extends \\{$class} { }
return {$newName}::className();
c;
        return eval($eval);
    }


    /** get the class name of a class/object
     *   Works both with static class:: calls and with $instance-> calls
    *   @return string
     *  @final
     */
    final public static function className()
    {
        return get_called_class();
    } // className

    /** get the Unqualified class name of a class/object
     *   Works both with static class:: calls and with $instance-> calls
    *   @return string
     *  @final
     */
    final public static function classNameUnqualified()
    {
        $c = get_called_class();
        $ns = strrpos($c, '\\');
        if (false !== $ns) {
            $class = substr($c, $ns+1);
        }
        return $class;
    } // classNameUnqualified


    /** get the namespace name of a class/object
     *  Works both with static class:: calls and with $instance-> calls
     *  @return string : namespace of the object's class, no leading/trailing \
     *  @final
     */
    final public static function namespaceName()
    {
        debug();
        $c = get_called_class();
        $p = strrpos($c, '\\');
        if (false === $p) {
            $ns = '';
        } else {
            $ns = substr($c, 0, $p);
        }
        return $ns;
    } // namespaceName


    /**
     *  get the last part of the class name, equal to filename w/o extension
    *       eg. "Class" for Yz_Class, "Console" for Yz_Application_Console
    *   @return string
    *   @final
    */
    final public static function shortClassName()
    {
        return substr( strrchr( get_called_class(), '_' ), 1 );
    } // shortClassName


    /** listAncestors: get an array of ancestor class names
     *  Start with the parent class upward to the topmost one (which is obviously Yz_Class)
    *   @return array[string]
    *   @final
    */
    final public static function listAncestors()
    {
        return class_parents(get_called_class());

        ###############
        $out = array();
        $class = get_parent_class();
        while (false !== $class) {
            $out[] = $class;
            $class = get_parent_class($class);
        }
        return $out;
    } // listAncestors


    /** call a getter for a property
    *   default to Exception when accessing an undefined variable or method
    *   @param scalar $var property name
    *   @return mixed
    *   @final
    */
    final public function __get( $var )
    {
        return $this->offsetGet($var);
    } // __get



    /** call a setter for a variable
    *  default to Exception when accessing an undefined variable or method
    *   @param scalar $var property name
    *   @param mixed $value
    *   @return chainable - ignored
    *   @final
    */
    final public function __set( $var, $value )
    {
        return $this->offsetSet($var, $value);
    } // set


    /**
    *  test if a property/key/index exists in the object
    *   @param scalar $var property name
    *   @return chainable - ignored
    *   @final
    */
    final public function __isset( $var )
    {
        return $this->offsetExists($var);
    } // __isset


    /**
    *  __unset()
    *   @param scalar $var
    *   @return chainable - ignored
    *   @final
    */
    final public function __unset( $var )
    {
        return $this->offsetUnset($var);
    } // __unset


    /** __call() : defaults to throw exception when an undefined method accessed
     *  @return null
    *   @throw Yz_Exception_InaccessibleMethod unconditionally
    */
    public function __call( $func, $args )
    {
        $ex = new Yz_Exception_InaccessibleMethod();
        throw $ex->withArgs( array(
            'methodName' => $func,
            'className' => $this->className(),
        ));
    } // __call


    /** __callStatic() : defaults to throw exception when an undefined static method accessed
     *  @return null
    *   @throw Yz_Exception_InaccessibleMethod unconditionally
    */
    public static function __callStatic( $func, $args )
    {
        $ex = new Yz_Exception_InaccessibleMethod();
        throw $ex->withArgs( array(
            'methodName' => $func,
            'className' => get_called_class(),
        ));
    } // __callStatic


    /** __sleep() : defaults to throw exception
     *  @return null
    *   @throw Yz_Exception_InaccessibleMethod unconditionally
    */
    public function __sleep()
    {
        $ex = new Yz_Exception_InaccessibleMethod();
        throw $ex->withArgs( array(
            'methodName' => '__sleep',
            'className' => $this->className(),
        ));
    }

    /** __wakeup() : defaults to throw exception
     *  @return null
    *   @throw Yz_Exception_InaccessibleMethod unconditionally
    */
    public function __wakeup()
    {
        $ex = new Yz_Exception_InaccessibleMethod();
        throw $ex->withArgs( array(
            'methodName' => '__wakeup',
            'className' => $this->className(),
        ));
    }

    /** __set_state() : defaults to throw exception
     *  @return null
    *   @throw Yz_Exception_InaccessibleMethod unconditionally
    */
    public static function __set_state( array $state )
    {
        $ex = new Yz_Exception_InaccessibleMethod();
        throw $ex->withArgs( array(
            'methodName' => '__set_state',
            'className' => get_called_class(),
        ));
    }


    /*******************************
    ****   ArrayAccess methods   ***
    *******************************/

    /** test if a key/index/property exists in the object
     *      Verify the index and forward the operation to
     *      <code>indexExists($var)</code>
     *  @param scalar $var
     *  @return bool
     *  @throw TypeMismatch if $var fails validation
     */
    public function offsetExists( $var )
    {
        $var = $this->adjustIndex( $var );
        return $this->indexExists( $var );
    }

    /** get a key/index/property
     *      Verify the index and forward the operation to
     *      <code>defaultGetter($var)</code>
     *  @param scalar $var
     *  @return mixed
     *  @throw TypeMismatch if $var fails validation
     */
    public function offsetGet( $var )
    {
        $var = $this->adjustIndex( $var );
        return $this->defaultGetter( $var );
    }

    /** set a key/index/property
     *      Verify the index and forward the operation to
     *      <code>defaultSetter($var, $value)</code>
     *  @param scalar $var
     *  @param mixed $value
     *  @return chainable
     *  @throw TypeMismatch if $var fails validation
     */
    public function offsetSet( $var, $value )
    {
        $var = $this->adjustIndex( $var );
        return $this->defaultSetter( $var, $value );
    }

    /** `unset' a key/index/property
     *      Verify the index and forward the operation to
     *      <code>defaultUnsetter($var, $value)</code>
     *  @param scalar $var
     *  @return chainable - mostly ignored
     *  @throw TypeMismatch if $var fails validation
     */
    public function offsetUnset( $var )
    {
        $var = $this->adjustIndex( $var );
        return $this->defaultUnsetter( $var );
    }


    /***********************
     *   Protected methods
     **********************/


    /**
    *   The method is called when an undefined Variable is accessed
     *  @param scalar $var will be handed over to the exception
     *  @param string $accessType will be handed over to the exception
    *   @return NEVER
    *   @throw Yz_Exception_InaccessibleProperty unconditionally
    */
    protected function undefinedVariable( $var, $accessType='get' )
    {
        $ex = new Yz_Exception_InaccessibleProperty();
        throw $ex->withArgs( array(
            'propertyName' => $var,
            'className' => $this->className(),
            'access' => $accessType,
        ));
    } // undefinedVariable



    /** validate the index, adjust it if required/possible
     *   @param mixed index
     *   @return mixed valid index
     *   @throw InvalidArgument if index not valid
     */
    protected function adjustIndex( $index )
    {
        return Yz_String::testNotEmpty( $index );
    }

    /** the default existence checker for properties
     *  @param string index
     *  @return false
     */
    protected function indexExists( $index )
    {
        return false;
    }

    /** the default getter for properties which don't have theirs own
     *  @param string index
     *  @return NEVER
     *  @throw InaccessibleProperty
     */
    protected function defaultGetter( $index )
    {
        return $this->undefinedVariable($index, 'get');
    }

    /** the default setter for properties which don't have theirs own
     *  @param string index
     *  @return NEVER
     *  @throw InaccessibleProperty
     */
    protected function defaultSetter( $index, $value )
    {
        return $this->undefinedVariable($index, 'set');
    }

    /** the default unsetter for properties which don't have theirs own
     *  @param string index
     *  @return NEVER
     *  @throw InaccessibleProperty
     */
    protected function defaultUnsetter( $index )
    {
        return $this->undefinedVariable($index, 'unset');
    }


} // class

